﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using JGLib.States;
using Microsoft.Xna.Framework.Graphics;
using TrafficTrouble.Core;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input.Touch;

namespace TrafficTrouble.States
{
    public class GameState : State
    {
        public Grid Level { get; set; }

        private int GridSquareSize { get; set; }
        private Vector2 GridTopLeft { get; set; }

        private Car CarDragged;
        private Vector2 CarDragOffset;
        private HistoryItem CarDragMove;
        private Vector2 DraggedCarLoc;

#if DEBUG
        Vector2 touchPos;
        bool debugging = false;
        TouchCollection touchCollection;
#endif

        private static Vector2 ButtonMargin { get { return new Vector2(10, 10); } }
        private IconButton MenuButton, UndoButton, HintButton, BackButton, ResetButton;
        private TextButton NumMovesButton;

        private enum Button { Undo, Reset, Hint, Back, Menu, None }
        private Button buttonPressed = Button.None;

        public GameState()
            : base(typeof(GameState))
        {
            this.CarDragged = null;
            History.Reset();

            this.InitButtons();
        }
        private void InitButtons()
        {
            UndoButton = new IconButton("<-"/*"←"*/, "Undo", true);
            ResetButton = new IconButton("R"/*"⟲"*/, "Reset", true);
            HintButton = new IconButton("->"/*"→"*/, "Hint", true);
            BackButton = new IconButton("^"/*"▲"*/, "Back", false);
            MenuButton = new IconButton("M"/*"≡"*/, "Menu", false);
            NumMovesButton = new TextButton("Moves: 00");

            UndoButton.Location = ButtonMargin;
            HintButton.Location = new Vector2(Game.Width - ButtonMargin.X - HintButton.Size.X, ButtonMargin.Y);
            float distBetweenTopButtons = HintButton.Location.X - (UndoButton.Location.X + UndoButton.Size.X);
            ResetButton.Location = new Vector2(UndoButton.Location.X + UndoButton.Size.X 
                + (distBetweenTopButtons - ResetButton.Size.X) / 2f,
                ButtonMargin.Y);

            BackButton.Location = new Vector2(ButtonMargin.X, Game.Height - ButtonMargin.Y - BackButton.Size.Y);
            MenuButton.Location = new Vector2(Game.Width - ButtonMargin.X - MenuButton.Size.X,
                Game.Height - ButtonMargin.Y - MenuButton.Size.Y);
            float distBetweenBottomButtons = MenuButton.Location.X - (BackButton.Location.X + BackButton.Size.X);
            NumMovesButton.Location = new Vector2(BackButton.Location.X + BackButton.Size.X
                + (distBetweenBottomButtons - NumMovesButton.Size.X) / 2f,
                Game.Height - ButtonMargin.Y - NumMovesButton.Size.Y);
        }

        protected override void LoadContent()
        {
            this.LoadTexture("Game/Car_2", 2);
            this.LoadTexture("Game/Car_Player_2", -2);
#if DEBUG
            this.LoadContent<SpriteFont>("MainMenu/MenuFont", true);
#endif
        }

        protected override void GetSwitchedTo(object prevStateID, object[] data)
        {
            if (prevStateID is Type)
            {
                if ((Type)prevStateID == typeof(LevelSelectionState) || (Type)prevStateID == typeof(LoadingState))
                {
                    if (data[0] is Grid) this.Level = (Grid)data[0];
                    else this.Level = Grid.FromFile((LevelDifficulty)data[0], (int)data[1]);

                    this.GridSquareSize = Game.Width / this.Level.Size;
                    this.GridTopLeft = new Vector2(0, (Game.Height - this.Level.Size * this.GridSquareSize) / 2);
                }
            }
            else if (prevStateID is Game.Dialog)
            {
                if ((Game.Dialog)prevStateID == Game.Dialog.Reset)
                {
                    bool reset = (int)data[0] == 0;
                    if (reset) this.RestartLevel();
                }
                else if ((Game.Dialog)prevStateID == Game.Dialog.BackToMenu)
                {
                    bool backToMenu = (int)data[0] == 0;
                    if (backToMenu)
                        this.SwitchToMenu();
                }
                // uncomment the following three lines once there is an actual level selection
                //else if ((Game.Dialog)prevStateID == Game.Dialog.BackToLevelSelection)
                //{

                //}
            }
        }

        #region Updating
        protected override void Update(double gameTime)
        {
#if DEBUG
            touchCollection = Touch.Touches;
            foreach (TouchLocation touch in touchCollection)
            {
#else
            foreach (TouchLocation touch in Touch.Touches)
            {
#endif
#if DEBUG
                touchPos = touch.Position;
                if (touch.State == TouchLocationState.Pressed
                    && touchPos.X < this.GridSquareSize / 2 
                    && touchPos.Y < this.GridSquareSize / 2)
                    debugging ^= true;
#endif
                checkButtonPresses(touch);

                if (this.CarDragged == null)
                {
                    this.checkStartCarDrag(touch);
                }
                else
                {
                    if (touch.State == TouchLocationState.Released)
                        this.releaseCar();
                    else if (touch.State == TouchLocationState.Moved)
                        slideCar(touch);
                }

                break;
            }

            base.Update(gameTime);
        }

        private void checkButtonPresses(TouchLocation touch)
        {
            Button buttonOver = this.getTouchedButton(touch);

            if (touch.State == TouchLocationState.Pressed)
            {
                this.buttonPressed = buttonOver;
                this.setButtonsPressed(this.buttonPressed, true);
            }
            else if (touch.State == TouchLocationState.Moved && buttonOver != this.buttonPressed)
            {
                this.setButtonsPressed(this.buttonPressed = Button.None, true);
            }
            else if (touch.State == TouchLocationState.Released)
            {
                if (this.buttonPressed == buttonOver && this.buttonPressed != Button.None)
                {
                    switch (buttonOver)
                    {
                        case Button.Undo: this.UndoMove(); break;
                        case Button.Reset:
                        {
                            // show dialog for "are you sure you want to reset?"
                            this.SwitchToState(Game.Dialog.Reset, this);
                            break;
                        }
                        case Button.Hint: break; // show the hint
                        case Button.Back: break; // go back to the level selection state
                        case Button.Menu:
                        {
                            // go back to the main menu -- show dialog if history length > 0
                            if (History.Length > 0)
                                this.SwitchToState(Game.Dialog.BackToMenu, this);
                            else
                                this.SwitchToMenu();
                            break;
                        }
                        default: throw new Exception("Forgot a button in TrafficTrouble.Core.GameState."
                            + "checkButtonPresses(TouchLocation).");
                    }
                }

                this.setButtonsPressed(this.buttonPressed = Button.None, true);
            }
        }
        private Button getTouchedButton(TouchLocation touch)
        {
            Button button = Button.None;

            if (UndoButton.ContainsLocation(touch.Position)) button = Button.Undo;
            else if (ResetButton.ContainsLocation(touch.Position)) button = Button.Reset;
            else if (HintButton.ContainsLocation(touch.Position)) button = Button.Hint;
            else if (BackButton.ContainsLocation(touch.Position)) button = Button.Back;
            else if (MenuButton.ContainsLocation(touch.Position)) button = Button.Menu;

            return button;
        }
        private void setButtonsPressed(Button buttonTouched, bool resetOthers)
        {
            UndoButton.IsPressed = buttonTouched == Button.Undo ? true : (resetOthers ? false : UndoButton.IsPressed);
            ResetButton.IsPressed = buttonTouched == Button.Reset ? true : (resetOthers ? false : ResetButton.IsPressed);
            HintButton.IsPressed = buttonTouched == Button.Hint ? true : (resetOthers ? false : HintButton.IsPressed);
            BackButton.IsPressed = buttonTouched == Button.Back ? true : (resetOthers ? false : BackButton.IsPressed);
            MenuButton.IsPressed = buttonTouched == Button.Menu ? true : (resetOthers ? false : MenuButton.IsPressed);
        }

        private void checkStartCarDrag(TouchLocation touch)
        {
            if (touch.State == TouchLocationState.Pressed)
            {
                Point touchLoc = this.GetGridCoords(touch.Position);
                Car car = this.Level.GetCarAt(touchLoc.X, touchLoc.Y);
                if (car != null) // if the user did NOT touch an empty space
                {
                    this.CarDragged = car;
                    this.CarDragOffset = touch.Position - (new Vector2(CarDragged.X, CarDragged.Y) * this.GridSquareSize)
                        - this.GridTopLeft;

                    this.CarDragMove = new HistoryItem(this.CarDragged, this.CarDragged.GridCoords, Point.Zero);
                    this.DraggedCarLoc = this.GetGridSquareTopLeft(car.GridCoords);
                }
            }
        }
        private void releaseCar()
        {
            // stop moving car, add move to history if its displacement was not zero,
            // and reset car-dragging variables.
            float carLoc = this.CarDragged.Horizontal ? this.DraggedCarLoc.X : this.DraggedCarLoc.Y;
            // basically stealing code from this.GetGridCoords(Vector2), but I need it to be a bit custom here.
            carLoc -= this.CarDragged.Horizontal ? this.GridTopLeft.X : this.GridTopLeft.Y;
            carLoc /= this.GridSquareSize;
            int carGridCoord = (int)Math.Round(carLoc);
            this.CarDragMove.End = this.CarDragged.Horizontal ? 
                new Point(carGridCoord, this.CarDragged.GridCoords.Y) :
                new Point(this.CarDragged.GridCoords.X, carGridCoord);
            this.CarDragged.GridCoords = this.CarDragMove.End;
            if (this.CarDragMove.Start != this.CarDragMove.End)
            {
                History.AddMove(this.CarDragMove);
                NumMovesButton.Text = "Moves: " + (History.Length < 10 ? "0" : "") + History.Length;
            }
            this.CarDragged = null;
        }

        private void slideCar(TouchLocation touch)
        {
            Point newCarLoc = this.GetGridCoordsFromClick(touch.Position);
            this.CarDragged.MoveAsFarAsAllowed(this.CarDragged.Horizontal ? newCarLoc.X : newCarLoc.Y);

            this.continousSlideCar(touch);
        }
        private void continousSlideCar(TouchLocation touch)
        {
            if (this.CarDragged.Horizontal)
            {
                // get the locations in the row of the dragged car that have a
                // direct line of sight with the dragged car.
                Tuple<int, int> rowEmptyExtents = this.Level.GetEmptyExtentsInInRow(this.CarDragged);
                // emptyExtents is the minimum and maximum X location, in pixels,
                // of where the dragged car can be. rowEmptyExtents.Item2 (the
                // maximum grid coord) must be decreased by (1 - car length) so
                // that the car does not barge into (or through) other cars.
                Tuple<float, float> emptyExtents = new Tuple<float, float>(
                    rowEmptyExtents.Item1 * this.GridSquareSize,
                    (rowEmptyExtents.Item2 - (this.CarDragged.Length - 1)) * this.GridSquareSize);
                // find where the current car would be in that range, using the touch location.
                float carLocX = touch.Position.X - this.CarDragOffset.X - this.GridTopLeft.X;
                // move the car back into its allowed range if it is out of it.
                if (carLocX < emptyExtents.Item1) carLocX = emptyExtents.Item1;
                if (carLocX > emptyExtents.Item2) carLocX = emptyExtents.Item2;

                // and finally set the position at which the car will be drawn
                this.DraggedCarLoc = new Vector2(carLocX, this.CarDragged.Y * this.GridSquareSize) + this.GridTopLeft;
            }
            else
            {
                // same comments as above, only change it from horizontal to vertical
                Tuple<int, int> colEmptyExtents = this.Level.GetEmptyExtentsInColumn(this.CarDragged);
                Tuple<float, float> emptyExtents = new Tuple<float, float>(
                    colEmptyExtents.Item1 * this.GridSquareSize,
                    (colEmptyExtents.Item2 - (this.CarDragged.Length - 1)) * this.GridSquareSize);
                float carLocY = touch.Position.Y - this.CarDragOffset.Y - this.GridTopLeft.Y;
                if (carLocY < emptyExtents.Item1) carLocY = emptyExtents.Item1;
                if (carLocY > emptyExtents.Item2) carLocY = emptyExtents.Item2;

                this.DraggedCarLoc = new Vector2(this.CarDragged.X * this.GridSquareSize, carLocY) + this.GridTopLeft;
            }
        }
        #endregion

        #region Non-Car-Sliding Functionality (Restart, Undo, Hint, Menu)
        private void RestartLevel()
        {
            this.CarDragged = null;
            this.Level = Grid.FromFile(this.Level.Difficulty + "/" + this.Level.Difficulty + this.Level.Number);
            History.Reset();
            NumMovesButton.Text = "Moves: 00";
        }
        private void UndoMove()
        {
            if (!History.HasHistory) return;
            HistoryItem lastMove = History.GetLastMove();

            if (lastMove.End != lastMove.Car.GridCoords)
                throw new Exception("WTF? (TrafficTrouble.States.GameState.UndoMove()).");

            lastMove.Car.GridCoords = lastMove.Start;
            NumMovesButton.Text = "Moves: " + (History.Length < 10 ? "0" : "") + History.Length;
        }
        private HistoryItem GetHint()
        {

            // TODO - write A* search algorithm for best solution and return first move of that solution.
            throw new NotImplementedException();
        }
        private void SwitchToMenu()
        {
            Level = null;
            CarDragged = null;
            History.Reset();
            this.SwitchToState(typeof(MainMenuState));
        }
        #endregion

        #region Drawing
        protected override void Draw(SpriteBatch g)
        {
#if DEBUG
            if (debugging)
                DrawGrid(g);
#endif
            this.DrawCars(g);
            this.DrawButtons(g);
#if DEBUG
            if (debugging)
            {
                Point coord = this.GetGridCoords(this.touchPos);
                if (touchCollection.Count > 0 && coord.X >= 0 && coord.Y >= 0 && coord.X < this.Level.Size && coord.Y < this.Level.Size)
                    g.DrawString(this.GetContent<SpriteFont>(true),
                    "(" + coord.X + "," + coord.Y + "):"
                    + (Level.IsEmpty(coord.X, coord.Y) ? "Empty" : ("[" + Level.GetCarAt(coord.X, coord.Y).ToString() + "]")),
                    touchPos, Color.White * .5f);
            }
#endif
        }
        public void Draw(SpriteBatch g, DialogState theOnlyStateThatShouldCallThisMethod)
        {
            this.Draw(g);
        }

#if DEBUG
        private void DrawGrid(SpriteBatch g)
        {
            Texture2D line = new Texture2D(Game.TrafficTroubleGame.GraphicsDevice, 1, 1);
            line.SetData<Color>(new Color[] { new Color(255, 255, 255, 255 / 2) });

            for (int x = 0; x <= this.Level.Size; x++)
                g.Draw(line, new Rectangle((int)this.GridTopLeft.X + x * this.GridSquareSize, (int)this.GridTopLeft.Y,
                    1, this.Level.Size * this.GridSquareSize), Color.White);
            // the right-most column will be one pixel smaller x-wise because the next line draws on its right edge,
            // and previously a line was drawn on its left edge as well.
            g.Draw(line, new Rectangle((int)this.GridTopLeft.X + this.Level.Size * this.GridSquareSize - 1, (int)this.GridTopLeft.Y,
                1, this.Level.Size * this.GridSquareSize), Color.White);
            for (int y = 0; y <= this.Level.Size; y++)
                g.Draw(line, new Rectangle((int)this.GridTopLeft.X, (int)this.GridTopLeft.Y + y * this.GridSquareSize,
                    this.Level.Size * this.GridSquareSize, 1), Color.White);
        }
#endif
        private void DrawCars(SpriteBatch g)
        {
            foreach (Car car in this.Level.Cars)
            {
                // Change the '2' to 'car.Length' 3 lines below this and delete these 3 comments once more car images are 'found'.
                //                                 | | | | | | | |
                //                                 V V V V V V V V
                Texture2D carTex = this.GetTexture(/*car.Length*/2 * (car.PlayerCar ? -1 : 1));
                Vector2 squareTopLeft = this.GetGridSquareTopLeft(car.GridCoords);

                if (car == this.CarDragged)
                    squareTopLeft = this.DraggedCarLoc;

                if (car.Horizontal)
                {
                    g.Draw(carTex, new Rectangle((int)squareTopLeft.X, (int)squareTopLeft.Y,
                            car.Length * this.GridSquareSize, this.GridSquareSize),
                        Color.White);
                }
                else
                {
                    // draw rotated 90 degrees to the right. (left?... whatever)
                    g.Draw(carTex, new Rectangle((int)squareTopLeft.X, (int)squareTopLeft.Y,
                            car.Length * this.GridSquareSize, this.GridSquareSize),
                        null,
                        Color.White,
                        MathHelper.PiOver2,
                        new Vector2(0, carTex.Height),
                        SpriteEffects.None,
                        0);
                }
            }
        }
        private void DrawButtons(SpriteBatch g)
        {
            UndoButton.Draw(g);
            ResetButton.Draw(g);
            HintButton.Draw(g);
            ResetButton.Draw(g);
            BackButton.Draw(g);
            NumMovesButton.Draw(g);
            MenuButton.Draw(g);
        }
        #endregion

        #region Utility Methods
        /// <summary>Gets the coordinates of the grid square of the pixel at the specified location on the screen.</summary>
        /// <param name="location">The location on the screen, in pixels.</param>
        /// <returns>The coordinates of the grid square.</returns>
        private Point GetGridCoords(Vector2 location)
        {
            Vector2 gridLoc = location - this.GridTopLeft;
            return new Point((int)(gridLoc.X / this.GridSquareSize),
                             (int)(gridLoc.Y / this.GridSquareSize));
        }
        /// <summary>Gets the coordinates of the grid square under a touch location, compensated for car drag offset.</summary>
        /// <param name="click">The location of the touch.</param>
        /// <returns>The coordinates of the grid square.</returns>
        private Point GetGridCoordsFromClick(Vector2 click)
        {
            return GetGridCoords(click - this.CarDragOffset);
        }

        /// <summary>Gets the location of the top-left corner of the specified grid square.</summary>
        /// <param name="gridCoords">The coordinates of the grid square.</param>
        /// <returns>The location of the top-left corner, in pixels.</returns>
        private Vector2 GetGridSquareTopLeft(Point gridCoords)
        {
            return new Vector2(this.GridTopLeft.X + gridCoords.X * this.GridSquareSize,
                               this.GridTopLeft.Y + gridCoords.Y * this.GridSquareSize);
        }

        public Texture2D GetCarTexture(int carTex, bool playerCar)
        {
            return this.GetTexture(carTex * (playerCar ? -1 : 1));
        }
        #endregion
    }
}
